<?php
/**
 * @link http://www.tintsoft.com/
 * @copyright Copyright (c) 2013 TintSoft LLC
 * @license http://www.tintsoft.com/license/
 */
namespace Leaps\Helper;
use Leaps;
use Leaps\Base\Arrayable;
use Leaps\Base\InvalidParamException;
/**
 * BaseArrayHelper 提供[[ArrayHelper]]具体的实现
 * 不要使用 BaseArrayHelper. 使用 [[ArrayHelper]]
 *
 * @author Tongle Xu <xutongle@gmail.com>
 * @since 5.0
 */
class BaseArrayHelper
{
    /**
     * 将一个对象或一个对象数组转换成一个数组。
     * @param object|array $object the object to be converted into an array
     * @param array $properties a mapping from object class names to the properties that need to put into the resulting arrays.
     * The properties specified for each class is an array of the following format:
     *
     * ~~~
     * [
     *     'app\models\Post' => [
     *         'id',
     *         'title',
     *         // the key name in array result => property name
     *         'createTime' => 'create_time',
     *         // the key name in array result => anonymous function
     *         'length' => function ($post) {
     *             return strlen($post->content);
     *         },
     *     ],
     * ]
     * ~~~
     *
     * The result of `ArrayHelper::toArray($post, $properties)` could be like the following:
     *
     * ~~~
     * [
     *     'id' => 123,
     *     'title' => 'test',
     *     'createTime' => '2013-01-01 12:00AM',
     *     'length' => 301,
     * ]
     * ~~~
     *
     * @param boolean $recursive whether to recursively converts properties which are objects into arrays.
     * @return array the array representation of the object
     */
    public static function toArray($object, $properties = [], $recursive = true)
    {
        if (!empty($properties) && is_object($object)) {
            $className = get_class($object);
            if (!empty($properties[$className])) {
                $result = [];
                foreach ($properties[$className] as $key => $name) {
                    if (is_int($key)) {
                        $result[$name] = $object->$name;
                    } else {
                        $result[$key] = static::getValue($object, $name);
                    }
                }
                return $result;
            }
        }
        if ($object instanceof Arrayable) {
            $object = $object->toArray();
            if (!$recursive) {
                return $object;
            }
        }
        $result = [];
        foreach ($object as $key => $value) {
            if ($recursive && (is_array($value) || is_object($value))) {
                $result[$key] = static::toArray($value, true);
            } else {
                $result[$key] = $value;
            }
        }
        return $result;
    }

    /**
     * 递归地对两个或多个数组合并为一个。
     * If each array has an element with the same string key value, the latter
     * will overwrite the former (different from array_merge_recursive).
     * Recursive merging will be conducted if both arrays have an element of array
     * type and are having the same key.
     * For integer-keyed elements, the elements from the latter array will
     * be appended to the former array.
     * @param array $a array to be merged to
     * @param array $b array to be merged from. You can specify additional
     * arrays via third argument, fourth argument etc.
     * @return array the merged array (the original arrays are not changed.)
     */
    public static function merge($a, $b)
    {
        $args = func_get_args();
        $res = array_shift($args);
        while (!empty($args)) {
            $next = array_shift($args);
            foreach ($next as $k => $v) {
                if (is_integer($k)) {
                    isset($res[$k]) ? $res[] = $v : $res[$k] = $v;
                } elseif (is_array($v) && isset($res[$k]) && is_array($res[$k])) {
                    $res[$k] = self::merge($res[$k], $v);
                } else {
                    $res[$k] = $v;
                }
            }
        }
        return $res;
    }

    /**
     * 获取一个数组元素的值或对象属性与给定的键或属性名称
     * 如果键数组中不存在,返回默认值。
     *
     * Below are some usage examples,
     *
     * ~~~
     * // working with array
     * $username = \Leaps\Helper\ArrayHelper::getValue($_POST, 'username');
     * // working with object
     * $username = \Leaps\Helper\ArrayHelper::getValue($user, 'username');
     * // working with anonymous function
     * $fullName = \Leaps\Helper\ArrayHelper::getValue($user, function($user, $defaultValue) {
     *     return $user->firstName . ' ' . $user->lastName;
     * });
     * ~~~
     *
     * @param array|object $array array or object to extract value from
     * @param string|\Closure $key key name of the array element, or property name of the object,
     * or an anonymous function returning the value. The anonymous function signature should be:
     * `function($array, $defaultValue)`.
     * @param mixed $default the default value to be returned if the specified key does not exist
     * @return mixed the value of the element if found, default value otherwise
     */
    public static function getValue($array, $key, $default = null)
    {
        if ($key instanceof \Closure) {
            return $key($array, $default);
        } elseif (is_array($array)) {
            return isset($array[$key]) || array_key_exists($key, $array) ? $array[$key] : $default;
        } else {
            return $array->$key;
        }
    }

    /**
     * 从数组中删除一个条目,并返回值。如果键不存在数组中,默认值将返回。
     *
     * Usage examples,
     *
     * ~~~
     * // $array = ['type' => 'A', 'options' => [1, 2]];
     * // working with array
     * $type = \Leaps\Helper\ArrayHelper::remove($array, 'type');
     * // $array content
     * // $array = ['options' => [1, 2]];
     * ~~~
     *
     * @param array $array the array to extract value from
     * @param string $key key name of the array element
     * @param mixed $default the default value to be returned if the specified key does not exist
     * @return mixed|null the value of the element if found, default value otherwise
     */
    public static function remove(&$array, $key, $default = null)
    {
        if (is_array($array) && (isset($array[$key]) || array_key_exists($key, $array))) {
            $value = $array[$key];
            unset($array[$key]);
            return $value;
        }
        return $default;
    }

    /**
     * 根据指定的关键索引数组。
     * 应该多维输入数组或对象数组。
     *
     * The key can be a key name of the sub-array, a property name of object, or an anonymous
     * function which returns the key value given an array element.
     *
     * If a key value is null, the corresponding array element will be discarded and not put in the result.
     *
     * For example,
     *
     * ~~~
     * $array = [
     *     ['id' => '123', 'data' => 'abc'],
     *     ['id' => '345', 'data' => 'def'],
     * ];
     * $result = ArrayHelper::index($array, 'id');
     * // the result is:
     * // [
     * //     '123' => ['id' => '123', 'data' => 'abc'],
     * //     '345' => ['id' => '345', 'data' => 'def'],
     * // ]
     *
     * // using anonymous function
     * $result = ArrayHelper::index($array, function ($element) {
     *     return $element['id'];
     * });
     * ~~~
     *
     * @param array $array the array that needs to be indexed
     * @param string|\Closure $key the column name or anonymous function whose result will be used to index the array
     * @return array the indexed array
     */
    public static function index($array, $key)
    {
        $result = [];
        foreach ($array as $element) {
            $value = static::getValue($element, $key);
            $result[$value] = $element;
        }
        return $result;
    }

    /**
     * 返回数组中指定的列的值。
     * 应该多维输入数组或对象数组。
     *
     * For example,
     *
     * ~~~
     * $array = [
     *     ['id' => '123', 'data' => 'abc'],
     *     ['id' => '345', 'data' => 'def'],
     * ];
     * $result = ArrayHelper::getColumn($array, 'id');
     * // the result is: ['123', '345']
     *
     * // using anonymous function
     * $result = ArrayHelper::getColumn($array, function ($element) {
     *     return $element['id'];
     * });
     * ~~~
     *
     * @param array $array
     * @param string|\Closure $name
     * @param boolean $keepKeys whether to maintain the array keys. If false, the resulting array
     * will be re-indexed with integers.
     * @return array the list of column values
     */
    public static function getColumn($array, $name, $keepKeys = true)
    {
        $result = [];
        if ($keepKeys) {
            foreach ($array as $k => $element) {
                $result[$k] = static::getValue($element, $name);
            }
        } else {
            foreach ($array as $element) {
                $result[] = static::getValue($element, $name);
            }
        }

        return $result;
    }

    /**
     * 构建一个映射(键-值对)多维数组或对象数组。
     * The `$from` and `$to` parameters specify the key names or property names to set up the map.
     * Optionally, one can further group the map according to a grouping field `$group`.
     *
     * For example,
     *
     * ~~~
     * $array = [
     *     ['id' => '123', 'name' => 'aaa', 'class' => 'x'],
     *     ['id' => '124', 'name' => 'bbb', 'class' => 'x'],
     *     ['id' => '345', 'name' => 'ccc', 'class' => 'y'],
     * );
     *
     * $result = ArrayHelper::map($array, 'id', 'name');
     * // the result is:
     * // [
     * //     '123' => 'aaa',
     * //     '124' => 'bbb',
     * //     '345' => 'ccc',
     * // ]
     *
     * $result = ArrayHelper::map($array, 'id', 'name', 'class');
     * // the result is:
     * // [
     * //     'x' => [
     * //         '123' => 'aaa',
     * //         '124' => 'bbb',
     * //     ],
     * //     'y' => [
     * //         '345' => 'ccc',
     * //     ],
     * // ]
     * ~~~
     *
     * @param array $array
     * @param string|\Closure $from
     * @param string|\Closure $to
     * @param string|\Closure $group
     * @return array
     */
    public static function map($array, $from, $to, $group = null)
    {
        $result = [];
        foreach ($array as $element) {
            $key = static::getValue($element, $from);
            $value = static::getValue($element, $to);
            if ($group !== null) {
                $result[static::getValue($element, $group)][$key] = $value;
            } else {
                $result[$key] = $value;
            }
        }
        return $result;
    }

    /**
     * 基于一个活多个键一个或多个键排序一个数组或对象或者多为数组
     * @param array $array the array to be sorted. The array will be modified after calling this method.
     * @param string|\Closure|array $key the key(s) to be sorted by. This refers to a key name of the sub-array
     * elements, a property name of the objects, or an anonymous function returning the values for comparison
     * purpose. The anonymous function signature should be: `function($item)`.
     * To sort by multiple keys, provide an array of keys here.
     * @param boolean|array $descending whether to sort in descending or ascending order. When
     * sorting by multiple keys with different descending orders, use an array of descending flags.
     * @param integer|array $sortFlag the PHP sort flag. Valid values include
     * `SORT_REGULAR`, `SORT_NUMERIC`, `SORT_STRING`, `SORT_LOCALE_STRING`, `SORT_NATURAL` and `SORT_FLAG_CASE`.
     * Please refer to [PHP manual](http://php.net/manual/en/function.sort.php)
     * for more details. When sorting by multiple keys with different sort flags, use an array of sort flags.
     * @param boolean|array $caseSensitive whether to sort string in case-sensitive manner. This parameter
     * is used only when `$sortFlag` is `SORT_STRING`.
     * When sorting by multiple keys with different case sensitivities, use an array of boolean values.
     * @throws InvalidParamException if the $descending or $sortFlag parameters do not have
     * correct number of elements as that of $key.
     */
    public static function multisort(&$array, $key, $descending = false, $sortFlag = SORT_REGULAR, $caseSensitive = true)
    {
        $keys = is_array($key) ? $key : [$key];
        if (empty($keys) || empty($array)) {
            return;
        }
        $n = count($keys);
        if (is_scalar($descending)) {
            $descending = array_fill(0, $n, $descending);
        } elseif (count($descending) !== $n) {
            throw new InvalidParamException('The length of $descending parameter must be the same as that of $keys.');
        }
        if (is_scalar($sortFlag)) {
            $sortFlag = array_fill(0, $n, $sortFlag);
        } elseif (count($sortFlag) !== $n) {
            throw new InvalidParamException('The length of $sortFlag parameter must be the same as that of $keys.');
        }
        if (is_scalar($caseSensitive)) {
            $caseSensitive = array_fill(0, $n, $caseSensitive);
        } elseif (count($caseSensitive) !== $n) {
            throw new InvalidParamException('The length of $caseSensitive parameter must be the same as that of $keys.');
        }
        $args = [];
        foreach ($keys as $i => $key) {
            $flag = $sortFlag[$i];
            $cs = $caseSensitive[$i];
            if (!$cs && ($flag === SORT_STRING)) {
                $flag = $flag | SORT_FLAG_CASE;
                $args[] = static::getColumn($array, $key);
            } else {
                $args[] = static::getColumn($array, $key);
            }
            $args[] = $descending[$i] ? SORT_DESC : SORT_ASC;
            $args[] = $flag;
        }
        $args[] = &$array;
        call_user_func_array('array_multisort', $args);
    }


    /**
     * 编码字符串数组中的特殊字符为HTML实体。
     * Both the array keys and values will be encoded.
     * If a value is an array, this method will also encode it recursively.
     * @param array $data data to be encoded
     * @param boolean $valuesOnly whether to encode array values only. If false,
     * both the array keys and array values will be encoded.
     * @param string $charset the charset that the data is using. If not set,
     * [[\Leaps\Base\Application::charset]] will be used.
     * @return array the encoded data
     * @see http://www.php.net/manual/en/function.htmlspecialchars.php
     */
    public static function htmlEncode($data, $valuesOnly = true, $charset = null)
    {
        if ($charset === null) {
            $charset = Leaps::$app->charset;
        }
        $d = [];
        foreach ($data as $key => $value) {
            if (!$valuesOnly && is_string($key)) {
                $key = htmlspecialchars($key, ENT_QUOTES, $charset);
            }
            if (is_string($value)) {
                $d[$key] = htmlspecialchars($value, ENT_QUOTES, $charset);
            } elseif (is_array($value)) {
                $d[$key] = static::htmlEncode($value, $charset);
            }
        }
        return $d;
    }


    /**
     * 解码成相应的HTML实体字符字符串数组。
     * Both the array keys and values will be decoded.
     * If a value is an array, this method will also decode it recursively.
     * @param array $data data to be decoded
     * @param boolean $valuesOnly whether to decode array values only. If false,
     * both the array keys and array values will be decoded.
     * @return array the decoded data
     * @see http://www.php.net/manual/en/function.htmlspecialchars-decode.php
     */
    public static function htmlDecode($data, $valuesOnly = true)
    {
        $d = [];
        foreach ($data as $key => $value) {
            if (!$valuesOnly && is_string($key)) {
                $key = htmlspecialchars_decode($key, ENT_QUOTES);
            }
            if (is_string($value)) {
                $d[$key] = htmlspecialchars_decode($value, ENT_QUOTES);
            } elseif (is_array($value)) {
                $d[$key] = static::htmlDecode($value);
            }
        }
        return $d;
    }
}